// Copyright 2009 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Author: Jeff Stewart
//
// Implements a probe that connects to an SMTP-MSA server.
// Probes all A records returned by DNS,
// but only probes as far as a banner check on all but the last record.

#ifndef POPDIAG_PROBES_SMTPPROBE_H__
#define POPDIAG_PROBES_SMTPPROBE_H__

#include <QtCore/QList>
#include <QtCore/QMap>
#include <QtCore/QSet>
#include <QtCore/QString>
#include <QtCore/QTextStream>
#include <QtCore/QTimer>
#include <QtNetwork/QHostInfo>
#include <QtNetwork/QTcpSocket>
#include <QtNetwork/QtNetwork>

#include "probes/probe.h"
#include "probes/multiprobe.h"
#include "probes/linereader.h"

namespace popdiag {

class PopProbe;
class SmtpResponseReader;

typedef enum {
  SMTP_RESPONSE_COMPLETE,   // parse succeeded.
  SMTP_RESPONSE_MALFORMED,
  SMTP_SOCKET_DROPPED,
  SMTP_RESPONSE_TIMEOUT } SmtpResponseType;

class SmtpProbeFactory : public MultiProbeFactory {
 public:
  // Creates a factory that passes these arguments to SmtpProbe, along
  // with the address provided in CreateProbe.
  SmtpProbeFactory(const QString &username,
                   const QString &password,
                   const QString &smtp_server_name,
                   int smtp_port);

  // Creates an SmtpProbe using the combination of these parameters and those
  // passed in the constructor.
  virtual Probe *CreateProbe(const QHostAddress &addr, bool do_complete_probe);
 private:
  const QString username_;
  const QString password_;
  const QString smtp_server_name_;
  const int smtp_port_;
};

class SmtpProbe : public Probe {
  Q_OBJECT
 public:
  // Creates a probe to exercise an SMTP server
  // username: The username to authenticate with using the SMTP AUTH command.
  // password: The password to authenticate with using the SMTP AUTH command.
  // smtp_server: DNS name of the host we're probing.  Used for SSL cert
  //              checks
  // host_address: The IPv4 address to connect to.
  // smtp_port: What port to probe?  Mode (TLS or SSL) is derived from this
  //            as well
  // probe_ehlo_only: If true, connect, establish SSL, and issue the secure
  //                  ehlo command, then stop.  Otherwise continue to
  //                  MAIL FROM, and RCPT TO.
  SmtpProbe(const QString &username,
            const QString &password,
            const QString &smtp_server,
            const QHostAddress &host_address,
            int smtp_port,
            bool probe_ehlo_only);
  virtual ~SmtpProbe();

  // values in the common range must match multiprobe's values
  typedef enum {
    // begin common range
    OK = 0,
    UNKNOWN = 1,
    RESOLVE_FAILED = 2,
    // end common range

    CONNECTION_REFUSED,
    CONNECT_TIMED_OUT,
    SERVER_CLOSED_PREMATURELY,
    SOCKET_ERROR,
    SMTP_SERVER_SHEDDING_LOAD,
    SMTP_BANNER_BAD_RESPCODE,
    SMTP_SERVER_RESPONSE_MALFORMED,
    SMTP_SERVER_RESPONSE_TIMED_OUT,
    SMTP_SERVER_BAD_RESPCODE,
    SMTP_SERVER_NOT_OFFERING_TLS,
    SMTP_SERVER_NOT_OFFERING_AUTH,
    SMTP_SERVER_CERT_SELFSIGNED,
    SMTP_SERVER_CERT_EXPIRED,
    SMTP_SERVER_CERT_NOTYETVALID,
    SMTP_SERVER_CERT_CANNOTVALIDATE,
    SMTP_SERVER_CERT_NAME_MISMATCH,
    SMTP_SERVER_CAFILE_NOT_FOUND,
    SMTP_SERVER_LOGIN_REJECTED,
    SMTP_SERVER_LOGIN_BAD_CODE,
    SMTP_SERVER_LOGIN_BACKEND_OFFLINE,
    SMTP_SERVER_REJECTED_MAIL_FROM,
    SMTP_SERVER_REJECTED_RCPT_TO,
    SMTP_SERVER_FAILED_RSET,
    SMTP_SERVER_REJECTED_QUIT,
  } SmtpProbeFault;

  // Fetch progress made in this probe.
  void GetProgress(int *progress, int *estimated_total);

  // Begin the probe.  Called by base class implementation.
  void StartProbe();

  // Get a display string describing the results of this probe.
  QString GetDisplayText();

  // Retrieve a numeric representation of this fault.
  // 0 is reserved for "success".  Most other values are probe-specific.
  int GetFaultId();

  // Get a string representation of this probe's class.
  // Can be used with GetFaultId() to construct support urls, though
  // it is not currently used for this.
  inline QString GetProbeId() { return "smtp"; }

 private slots:
  // Callback for Qt's QSslSocket:
  void OnSslError(const QList<QSslError> &errors);
  void SslTimeout();
  void OnError(QAbstractSocket::SocketError err);

  void OnConnected();
  void OnSslEstablished();
  void OnDisconnected();
  void OnConnectTimedOut();
  void OnConnectError(QAbstractSocket::SocketError e);

  void FinishCommand(SmtpResponseReader *reader);

 private:
  typedef enum {
    INIT,
    RESOLVE,  // will go away
    CONNECT,
    BANNER,
    EHLO,
    STARTTLS,
    NEGOTIATESSL,
    REEHLO,
    AUTH,
    MAILFROM,
    RCPTTO,
    RSET,
    QUIT,
    DISCONNECTED
  } TestState;

  const char *StepNameFromState();

  SmtpResponseReader *GetReader();

  void SetState(TestState t);
  void Cleanup();

  void ProcessSmtpResponse(SmtpResponseReader *reader);
  void ProbeDone(SmtpProbeFault f);

  // These are called in sequence:
  void StartProbeConnect();
  void StartProbeBanner();
  void ProcessBannerResponse(SmtpResponseReader *reader);

  void StartProbeEhlo();
  void ProcessEhloResponse(SmtpResponseReader *reader);

  void StartProbeStartTls();
  void ProcessStartTlsResponse(SmtpResponseReader *reader);

  void StartProbeNegotiateTls();

  void StartProbeAuth();
  void ProcessAuthResponse(SmtpResponseReader *reader);

  void StartProbeMailFrom();
  void ProcessMailFromResponse(SmtpResponseReader *reader);

  void StartProbeRcptTo();
  void ProcessRcptToResponse(SmtpResponseReader *reader);

  void StartProbeData();
  void ProcessDataResponse(SmtpResponseReader *reader);

  void SendData();
  void ProcessSendDataResponse(SmtpResponseReader *reader);

  void StartProbeRset();
  void ProcessRsetResponse(SmtpResponseReader *reader);

  void StartProbeQuit();
  void ProcessQuitResponse(SmtpResponseReader *reader);

  // how long will socket operations take to timeout?
  static const int kTimeout;

  // username to authenticate to SMTP server with
  const QString username_;

  // password to authenticate to SMTP server with
  const QString password_;

  // DNS name of SMTP server to connect to
  const QString smtp_server_;

  // TCP port to connect to.  Mode (TLS or SSL)
  // is derived from this port (465 == SSL, others == TLS)
  const int smtp_port_;

  // The base socket object.  Sometimes NULL.
  scoped_ptr<RenamableSslSocket> base_socket_;

  // Read and write from here.
  RenamableSslSocket *src_;

  // how far in our state machine have we gotten?
  TestState state_;

  // max value for state_.  Used for progress displays.
  const TestState max_state_;

  // What address are we going to attempt to connect to?
  const QHostAddress host_address_;

  // times our connect attempts for timeouts:
  QTimer *connect_timer_;

  // times out our SSL negotiation:
  QTimer *ssl_negotiation_timer_;

  // The fault we found in this test.
  SmtpProbeFault fault_;

  // should we stop after verifying the banner?
  const bool probe_ehlo_only_;

  // if true, we closed the socket.  Don't log
  // socket errors if this is true.
  bool we_closed_;
};

class SmtpResponseReader : public QObject {
  Q_OBJECT
 public:
  SmtpResponseReader(QObject *parent,
                     RenamableSslSocket *src, int timeout_in_millis);
  ~SmtpResponseReader();

  // Send a command and start reading the response.
  // The empty string is a special case.  It will start reading a response
  // without sending a command.
  void SendCommand(const QString &cmd);

  // What kind of response did we get?
  SmtpResponseType response_type() const { return response_type_; }

  // What was the SMTP response code?
  int response_code() const { return resp_code_; }

  // Fetch the lines of the response.
  const QList<QString> &response_lines() const { return response_lines_; }

  // Fetch the SMTP extended response codes, if any (4.7.2, etc.)
  const QSet<QString> &extended_codes() const { return extended_codes_; }

  // Return an entry suitable for the debug log.
  QString describe();

 signals:
  // emitted when this response is complete, either successfully or not.
  void ResponseRead(SmtpResponseReader *me);

  private slots:
  void DataAvailable(LineReadResult r);

 private:
  void TryRead();
  void ProcessLine(const QString &line);
  void Done(SmtpResponseType result);

  RenamableSslSocket *socket_;
  QList<QString> response_lines_;
  QSet<QString> extended_codes_;
  SmtpResponseType response_type_;

  bool anything_read_;
  int resp_code_;
  LineReader reader_;
};

}  // namespace popdiag

#endif  // POPDIAG_PROBES_SMTPPROBE_H__
